iT邦幫忙

2022 iThome 鐵人賽

DAY 10
0
Modern Web

真的好想離開 Vue 3 新手村 feat. CompositionAPI系列 第 10

真的好想離開 Vue 3 新手村 - Day 10: 從原生 JS 理解 Vue 3 響應式基礎 - reactive & ref (上)

  • 分享至 

  • xImage
  •  

前言

一開始認識 reactive()ref() 真的超困惑,不理解兩者背後的差別,只能硬記個別的使用方式和限制,一直到了解他們背的原理,才有「啊哈~」的感覺,一切都變得合理起來。
所以這個主題會先從原生 Javascript 開始談起,再來連結到 reactive()ref() 的特性與限制。

Outline

  • 什麼是響應式
  • 來點原生 Javascript 吧!
    • get、set、Object.defineProperty()
    • Proxy
  • reactive() 響應原理
  • ref() 響應原理

註:reactive()ref() 的特性與限制會在下一篇討論。

什麼是響應式(Reactivity)?

「響應式」指的是 Vue 幫助你即時更新相依資料這件事。
要做到這件事,其中一個要解決的問題就是,「要怎麼知道資料被改動了?」,Vue 3 推出的 reactive()ref() 就是用來攔截資料的讀取跟寫入,所以在開發上所有需要響應性的資料,都需要傳入 reactive()ref() 來處理。

reactive() 是用 Proxy 來實作,ref() 則是用 getset 關鍵字來實作。

所以在進入 reactive()ref() 之前,先來點原生 Javascript。

來點原生 Javascript

getter, setter

每次存取物件屬性,實際上會呼叫取值器(getter)。
每次對物件屬性做賦值,實際上會呼叫設值器(setter)。

原生 Javascript 可以透過物件實字或 Object.defineProperty,搭配 getset 關鍵字,重新去定義特定屬性的 getter 和 setter,從而改變讀取特定屬性對特定屬性賦值時的行為。

以下示範的是透過物件實字建立:

const obj = {
  value: "hello",
  //每次讀取 obj.message 時會呼叫
  get message1() {
    console.log("有人用了物件的 getter 讀取 message1");
    return this.value;
  },
  //每次對 obj.message 重新賦值會呼叫
  set message1(newValue) {
    console.log(`有人將物件的 message1 重新賦值為 ${newValue}`);
    this.value = newValue;
  },
};
let foo = obj.message1;
//印出 有人用了物件的 getter 讀取 message
obj.message1 = "你好";
//印出 有人將物件的 message 重新賦值為 你好

針對已經建立的物件,直接新增屬性是沒有用的,新增當下並沒有定義好 getter 和 setter:

obj.newProperty = "新增屬性";

想要新增屬性並自定義 getter 和 setter,需要透過 Object.defineProperty

Object.defineProperty(obj, property, descriptor)

延續使用前面實字建立的物件,針對該物件新增屬性和描述(getter & setter)

let value2 = "everyone";
Object.defineProperty(obj, "message2", {
  get: function () {
    console.log("有人用了物件的 getter 讀取 message2");
    return value2;
  },
  set: function (newValue) {
    console.log(`有人將物件的 message2 重新賦值為 ${newValue}`);
    value2 = newValue;
  },
});

obj.message2 = "Hey hey hey";
// 呼叫 setter
// 印出 有人將物件的 message 重新賦值為 Hey hey hey

缺點:

  • 不能直接使用在基本型別上
  • 每次只能對單一屬性設定 getter 跟 setter
  • 不能觀察物件屬性的變化:「新增屬性」或「移除屬性」
  • 承上,難以觀察陣列的變動

註:在 Vue 2 的時候,是透過 Object.defineProperty 來實作響應性。

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Vue 2 - 深入响应式原理

基於 Object.defineProperty 的限制,Vue 2 對於響應式物件和陣列的操作有一些的限制,好奇的人可以再去了解,這裡就不細說。

Proxy

Proxy 是原生 Javascript 的語法,翻譯為「代理」,只要透過 Proxy 代理操作物件,就可以攔截對物件的所有操作,不只讀取或寫入物件屬性,還包括迭代物件、使用 in 運算子、透過關鍵字 new 建立物件實體等等。

Syntax:

const proxy = new Proxy(target, handler);
  • target: 一般物件,想要攔截操作的目標
const target = {
  message1: "hello",
  message2: "everyone"
};
  • handler: 又稱為 trap,也就是每次透過代理操作物件時,會觸發的方法;如下面的程式碼,就是每次透過 obj.propertyName 取值時,會觸發 get trap,還有對物件屬性做賦值時,會觸發 set trap。
    可以從 MDN 裡找到所有提供的 trap,點這裡
const handler = {
  get(target, property, receiver) {
    console.log(`有人用了物件的 getter 讀取 ${property}`);
    return Reflect.get(...arguments);
  },
  set(target, property, value) {
    console.log(`有人將物件的 ${property} 賦值為 ${value}`);
    target[property] = value;
    return true;
  },
};

將 target 和 handler 傳進去,會建立一個代理 target 的 Proxy 物件。

  • 透過 Proxy 物件寫入底下的任何屬性,包括新增屬性,也會觸發 set handler
  • 透過 Proxy 物件讀取底下的任何屬性,都會經觸發 get handler
  • 直接對原物件操作不會觸發 handler
const proxy = new Proxy(target, handler);
proxy.newItem = "新屬性";
//會印出: 有人將物件的 newItem 賦值為 新屬性

myMessage = proxy.message1
//透過 proxy 讀取 target 物件
//會印出:有人用了物件的 getter 讀取 message1

myMessage = target.message1
//直接讀取 target 物件
//不會觸發 handler

Proxy 對 get 和 set 的 trap 是針對物件下所有屬性的讀取和寫入,所以很全面,以 array 資料來會更有感:

//續用上面的 handler
const todoArr = ["eat", "sleep", "coding"];
const arrProxy = new Proxy(todoArr, handler);

arrProxy.push("rolling");

//依序印出:
//有人用了物件的 getter 讀取 push
//有人用了物件的 getter 讀取 length
//有人將物件的 3 賦值為 rolling
//有人將物件的 length 賦值為 4

當我們用 push 新增了一個項目到陣列,其實不只是對屬性為 3 的項目進行賦值,同時還更動了陣列底下的 length,透過 Proxy 操作就全部都會觸發自訂的 handler。

註:如果對陣列 method 和 length 屬性的關係感覺疑惑,推薦看這篇 Day 30 咩色用得好 - 所以我說...陣列到底是什麼? 還有整個系列。

getter, setter v.s Proxy

通過上面的範例,可以了解到 Proxy 的優勢,以及 getter, setter 的不足之處(應該說 Proxy 大勝阿~)。

getter+setter Proxy
適用型別 物件 物件
可以攔截到的操作 單一屬性的讀取跟寫入 全部屬性的讀取跟寫入,甚至其他對物件的操作
能攔截到「新增」或「刪除」屬性 不行 可以

今天先簡單說明 reactiveref 的響應原理,明天再來細看並比較兩者的特性。

reactive

reactive() 是用 Proxy 來實作,主要是拿來處理物件型別資料的響應性。

響應原理

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key) //簡單來說,是用來紀錄相依邏輯和資料
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key) //簡單來說,是用來執行相依邏輯和更新資料
    }
  })
}

將物件型別資料傳入 reactive(),會回傳一個相對應的 Proxy 物件

  • 透過 Proxy「攔截」對原物件的操作,從而做到響應式
  • 所以對原物件操作是無效的,對 Proxy 代理物件操作才能有效響應
  • reactive() 傳入相同的物件或已經存在的 Proxy,會回傳相同的 Proxy,這個是 Vue 幫忙做的,以免同個物件有多個代碼,在原生的情況下並不會相等。
const rawObject = {};
const proxy = reactive(rawObject);
console.log(reactive(rawObject) === proxy); // true
console.log(reactive(proxy) === proxy); // true

ref

ref() 是用 getter 和 setter 來實作,主要是拿來處理基本型別資料的響應性,但是他也可以接受物件型別。(有沒有很迷惑的感覺XD)

/images/emoticon/emoticon19.gif等等,前面不是說 getter 和 setter 沒辦法用在基本型別上嗎?

沒錯,所以 ref 不是直接讓基本型別變成響應性(原生 Javascript 本身就做不到),而是透過把基本型別放到物件的 value 屬性下,來追蹤資料的變更。

響應原理

function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value') //簡單來說,是用來紀錄相依邏輯和資料
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value') //簡單來說,是用來執行相依邏輯和更新資料
    }
  }
  return refObject
}
  1. 會將傳入的資料放在 RefImpl 物件的 value 屬性下,並回傳 RefImpl 物件
  2. 對物件的 value 屬性定義 getter 跟 setter,去攔截對 value 的讀取跟寫入。
  3. 透過 ref() 創造回傳的 reference,來維持響應性,而不是資料(value)本身,所以基本型別才能透過 ref() 來達成響應
const luckyNumber = ref(7);
console.log(`luckyNumber:`, luckyNumber);

所以 ref 可以接收物件型別,為什麼我們還需要 reactive?
將物件型別傳入 ref ,實際上,響應性還是靠 reactive 達成的。

  • 當傳入的參數為物件型別,ref 會利用 reactive 轉換此物件的 .value
//ref 遇到物件型別,會用 reactive 去轉換再裝到 value 屬性下
const innerObj = { name: "innerObj" };
const refInnerObj = ref(innerObj);
console.log(`refInnerObj`, refInnerObj);

murmur:
其實本來沒有計畫分上下篇的,想在同一篇內一起說明兩個 API 的特性和限制,但是字數實在太多拉QQ
今天就先講到這裡,明天再繼續認識 ref()reactive() (遠目
/images/emoticon/emoticon06.gif

參考資料


上一篇
真的好想離開 Vue 3 新手村 - Day 9: v-for 與他的坑 feat. key & v-if
下一篇
真的好想離開 Vue 3 新手村 - Day 11: 從原生 JS 理解 Vue 3 響應式基礎 - reactive & ref (下)
系列文
真的好想離開 Vue 3 新手村 feat. CompositionAPI31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言